﻿using Daemon.Common.Cache;
using Daemon.Data.Infrastructure.Configuration;
using Daemon.Data.Substructure.Enums;
using Daemon.Repository.Contract;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Daemon.Repository.EF
{
    public class SessionItemRepository
    {
        private const string SessionsCacheKeyPrefix = "Sessions";

        private readonly IUserSessionRepository _userSessionRepository;

        private readonly IDaemonDistributedCache _daemonDistributedCache;

        private readonly IConfiguration _configuration;

        private readonly IContextInfoProvider _contextInfoProvider;

        private readonly string _clientSessionsCacheKey;

        private readonly int _heartbeatInterval;

        private readonly int _ttlSeconds;

        public SessionItemRepository(IDaemonDistributedCache daemonDistributedCache,
            IConfiguration configuration,
            IUserSessionRepository userSessionRepository,
            IContextInfoProvider contextInfoProvider)
        {
            _daemonDistributedCache = daemonDistributedCache;
            _clientSessionsCacheKey = $"{SessionsCacheKeyPrefix}";
            _configuration = configuration;
            _heartbeatInterval = int.Parse(_configuration["heartBeatInterval"] ?? "60");
            _ttlSeconds = _heartbeatInterval + int.Parse(_configuration["heartBeatIntervalBuffer"] ?? "60");
            _userSessionRepository = userSessionRepository;
            _contextInfoProvider = contextInfoProvider;
        }

        public bool CheckUserIsOnline(int userId)
        {
            Refresh();
            return GetOnlineUsers().Any(i => i.UserId == userId);
        }

        public void DeleteUserSession(int userId, LogoutReasonEnum logoutReason)
        {
            DeleteByUserId(userId, logoutReason);
            _userSessionRepository.SetLogoutTimeByUser(userId, logoutReason);
        }

        /// <summary>
        /// Delete session.
        /// </summary>
        /// <param name="sessionID">Current session item which tracked</param>
        /// <param name="logoutReason">Logout reason</param>
        /// <returns></returns>
        public bool Delete(string sessionID, LogoutReasonEnum? logoutReason = null)
        {
            Refresh();
            RemoveCache(sessionID, logoutReason ?? LogoutReasonEnum.Unknown);
            return true;
        }

        /// <summary>
        /// Delete session by user id.
        /// </summary>
        /// <param name="userid"></param>
        public void DeleteByUserId(int userid, LogoutReasonEnum logoutReason)
        {
            Refresh();
            var sessionItems = GetOnlineUsers().Where(i => i.UserId == userid);
            foreach (var sessionItem in sessionItems)
            {
                RemoveCache(sessionItem.Id, logoutReason);
            }
        }

        /// <summary>
        /// Register a <see cref="SessionItem"/>.
        /// </summary>
        /// <param name="sessionItem">The <see cref="SessionItem"/> which need be registered.</param>
        /// <returns>The registered <see cref="SessionItem"/> with a given Id.</returns>
        public SessionItem Register(SessionItem sessionItem, string prefix)
        {
            Refresh();
            sessionItem = InitSessionItem(sessionItem, prefix);
            AddOrUpdateCache(sessionItem.Id, sessionItem);
            _userSessionRepository.AddNewSessionIfNoLive();
            return sessionItem;
        }

        /// <summary>
        /// Update the existed <see cref="SessionItem"/>.
        /// </summary>
        /// <param name="sessionItem">The <see cref="SessionItem"/> which need be updated.</param>
        /// <returns>The updated <see cref="SessionItem"/> with the new live time.</returns>
        public SessionItem Update(SessionItem sessionItem, string prefix)
        {
            Refresh();
            sessionItem = InitSessionItem(sessionItem, prefix);
            AddOrUpdateCache(sessionItem.Id, sessionItem);
            return sessionItem;
        }

        private SessionItem InitSessionItem(SessionItem sessionItem, string prefix)
        {
            sessionItem.Id = string.IsNullOrEmpty(sessionItem.Id) ? Guid.NewGuid().ToString() : sessionItem.Id;
            sessionItem.LiveTime = DateTime.Now;
            sessionItem.UserId = _contextInfoProvider.AuthInfo.UserId;
            sessionItem.HeartBeatInterval = _heartbeatInterval;
            sessionItem.Prefix = prefix;

            return sessionItem;
        }

        private void AddOrUpdateCache(string sessionId, SessionItem sessionItem)
        {
            // On exception user session will be inaccurate until next successful update
            try
            {
                _daemonDistributedCache.HashSet(_clientSessionsCacheKey, sessionId, sessionItem);
            }
            catch (Exception rte)
            {
                Console.WriteLine($"Redis timeout while updating user session {sessionId}.", rte);
            }

        }

        private IEnumerable<SessionItem> GetOnlineUsers()
        {
            // On exception an empty set of sessions is returned and dependent logic will see no online users
            try
            {
                var users = _daemonDistributedCache.HashGetAll<SessionItem>(_clientSessionsCacheKey).Select(r => r.Value);
                return users;
            }
            catch (Exception)
            {

            }

            return new List<SessionItem>();
        }

        private void RemoveCache(string sessionId, LogoutReasonEnum logoutReason)
        {
            // On exception user session will remain until it times out and is removed by successful refresh
            try
            {
                var sessionItem = _daemonDistributedCache.HashGet<SessionItem>(_clientSessionsCacheKey, sessionId);
                _userSessionRepository.UpdateLogoutTimeForSession(new List<int> { sessionItem.UserId }, logoutReason);
                _daemonDistributedCache.HashDelete<SessionItem>(_clientSessionsCacheKey, sessionId);
            }
            catch (Exception re)
            {
                Console.WriteLine(re.Message);
            }
        }

        private bool IsLive(SessionItem session)
        {
            return session.LiveTime.HasValue && session.LiveTime.Value.AddSeconds(_ttlSeconds) > DateTime.Now;
        }

        private void Refresh()
        {
            // On exception user sessions will momentarily be inaccurate until next successful refresh
            try
            {
                var clientSessions = _daemonDistributedCache.HashGetAll<SessionItem>(_clientSessionsCacheKey);
                var deadUserIds = clientSessions.GroupBy(s => s.Value.UserId).Where(g => g.All(s => !IsLive(s.Value))).Select(g => g.Key);
                var deadSessionFields = clientSessions.Where(i => !IsLive(i.Value)).Select(i => i.Key).ToList();
                _userSessionRepository.UpdateLogoutTimeForSession(deadUserIds.ToList(), LogoutReasonEnum.SessionTimeout);
                _daemonDistributedCache.HashDelete<SessionItem>(_clientSessionsCacheKey, deadSessionFields);
            }
            catch (Exception)
            {

            }
        }
    }

    /// <summary>
    /// Defines session item.
    /// </summary>
    public class SessionItem
    {

        /// <summary>
        /// Gets or sets id.
        /// </summary>
        public string Id { get; set; }

        /// <summary>
        /// Gets or sets user id.
        /// </summary>
        public int UserId { get; set; }

        /// <summary>
        /// Gets the interval time heart beat in seconds.
        /// </summary>
        public int HeartBeatInterval { get; set; }

        /// <summary>
        /// Gets or sets the live time.
        /// </summary>
        public DateTime? LiveTime { get; set; }

        /// <summary>
        /// Gets or sets the prefix
        /// </summary>
        public string Prefix { get; set; }
    }
}
